package Thread;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @Author: 逸尘风清
 * @Date: 2021/4/26
 * @Time: 21:39
 * 描述： 单例模式使用双重验证，防止多线程出现问题（下面会进行解释）
 */
public class Volatile2 {
    private static Volatile2 INSTANCE;
    // private static volatile Volatile2 INSTANCE;   // 开启volatile 代码最后一步一步讲解

    private Volatile2(){

    }
    /**
    public static Volatile2 getINSTANCE(){
        if(INSTANCE == null){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Volatile2();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i = 1; i <= 100; i++){
            new Thread(()->{
                System.out.println(Volatile2.getINSTANCE().hashCode());
            }).start();
        }
    }
     */

    /**
     * 以上运行结果显示: 每个线程输出值都不一样，但是单例的理论是只能有一个实例对象，说明多线程情况下，这种写法会出错
     * 以下是使用synchronize锁来解决
     */

    /**
    public synchronized static Volatile2 getINSTANCE(){
        // 业务代码
        if(INSTANCE == null){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Volatile2();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i = 1; i <= 100; i++){
            new Thread(()->{
                System.out.println(Volatile2.getINSTANCE().hashCode());
            }).start();
        }
    }
     */
    /**
     * 以上结果显示输出结果都是一致的。但是这样的写法有待优化。
     * 原因：synchronize是将整个方法给锁住了，如果里面还有很多业务代码，那么显然这样就不好。锁的要求是锁住的代码范围越小越好
     * 以下是优化后的代码
     */

    /**
    public static Volatile2 getINSTANCE(){
        // 业务代码
        if(INSTANCE == null){
            synchronized (Volatile2.class){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Volatile2();
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i = 1; i <= 100; i++){
            new Thread(()->{
                System.out.println(Volatile2.getINSTANCE().hashCode());
            }).start();
        }
    }
     */

    /**
     * 将锁的范围改变之后发现运行的结果又不对了，为什么呢？
     * 原因：假设有两个线程，第一个线程刚进来的时候，发现实例是null，那么就会拿到锁进行实例化，而在第一个线程还没有实例化完成的时候，第二个线程进来了，那么第二个线程此时获取到的实例也是null。  等第一个线程实例完成后释放了锁，第二个线程拿到锁又进去实例化了，这样就导致了单例被重复实例化。
     * 解决办法：双重验证。看下面的代码
     */

    /**
    public static Volatile2 getINSTANCE(){
        // 业务代码
        if(INSTANCE == null){
            synchronized (Volatile2.class){
                if(INSTANCE == null) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Volatile2();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i = 1; i <= 100; i++){
            new Thread(()->{
                System.out.println(Volatile2.getINSTANCE().hashCode());
            }).start();
        }
    }
     */

    /**
     * 以上代码运行结果完全一致，即实现了锁代码的优化，又实现了多线程单例不会被多次实例化。
     * 使用的是双重验证；即使发生了上面说到的情况，只要再次进行判断，那么这时候永远都不会为空，会得到第一个进程的实例
     * 那么有个问题，既然这样可以解决线程高并发单例重复实例化的问题，还需不需开启volatile呢？
     * 答案：需要，如果不加问题会出现在  指令重排序  上面
     * 分析： new Volatile2()实例化的时候会进行三步操作
     * 1.给对象申请内存
     * 2.初始化对象成员变量
     * 3.将对象内容赋值给INSTANCE
     * 当指令重排序后，可能在还没有执行到第3步的时候，对象就已经赋值给INSTANCE了，也就是说实例化进行到了一半。假设这时候第二个线程进来，那么它判断INSTANCE已经不为空了。这时候第二个线程拿到的值就有问题。
     * 所以仍然需要开启volatile，就会禁止指令重新排序
     *
     * volatile虽然是保证可见性，但是并不能保证原子性，并不能代替synchronize，以下代码可以解释
     */

    /**
    volatile int count = 0;
    public void m(){
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        Volatile2 volatile2 = new Volatile2();
        // 创建线程列表
        List<Thread> threadList = new LinkedList<>();
        // 添加10个线程
        for (int i = 0; i < 10; i++) {
            threadList.add(new Thread(volatile2::m, "thread" + i));
        }
        threadList.forEach((o)->o.start());  // 循环开启线程
        threadList.forEach((o)->{
            try {
                o.join(); // 在线程里面加入线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(volatile2.count);
    }
    */
    /**
     * 以上不管程序运行多少次，每次结果都不一样。都达不到100000.
     * 原因：volatile只能保证线程可见性，当值发生改变时，其他所有的线程都可以接收到。但是并不能保证原子性
     * 假设线程1将count的值改成了1，那么线程2和线程3和其他所有线程都知道。假设线程2和线程3同时执行，那么他两获取到的值都是1，都进行count++，count变成2而不是3。所以程序执行结果会不准确。
     * 解决方案： 加上synchronize，如下代码
     */

    /**
    volatile int count = 0;
    public synchronized void m(){
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        Volatile2 volatile2 = new Volatile2();
        // 创建线程列表
        List<Thread> threadList = new LinkedList<>();
        // 添加10个线程
        for (int i = 0; i < 10; i++) {
            threadList.add(new Thread(volatile2::m, "thread" + i));
        }
        threadList.forEach((o)->o.start());  // 循环开启线程
        threadList.forEach((o)->{
            try {
                o.join(); // 在线程里面加入线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(volatile2.count);
    }
     */

    /**
     * 以上代码运行结果永远是100000
     */

    /**
     * synchronize锁优化原则：
     * 1.锁细化
     * 以下例子，假设要求只是需要对count++进行加锁，那么方法m2的写法就是锁细化
     * 2.锁粗化
     * 如果一个方法中含有非常多的锁，这样执行效率较低，干脆将整个方法锁起来，去除里面细化的锁
     *
     *锁细化和锁粗化是相对的，看场景
     */

    int count = 0;
    public synchronized void m1(){
        // 使用睡眠模拟业务代码执行时间
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        count++;

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public void m2(){
        // 使用睡眠模拟业务代码执行时间
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized(this){
            count++;
        }

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
